package edu.mapi.ir.viewer;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.util.LinkedHashMap;

import javax.swing.JPanel;
/**
 * @author ZP
 */
public class MazePanel extends JPanel implements ComponentListener {
	
	private Maze maze;
	private LinkedHashMap<String,MousePlayer> mice = new LinkedHashMap<String, MousePlayer>();
	private LinkedHashMap<IMapLayer, BufferedImage> layers = new LinkedHashMap<IMapLayer, BufferedImage>();
	private BufferedImage tmp = new BufferedImage(500, 500, BufferedImage.TYPE_INT_ARGB);
	private MousePaths pathLayer = null;
	private boolean gridEnabled = false, beaconVisEnabled = true, pathsVisible = true;
	protected double zoom = 15;
	private boolean autoZoom = true;
	private boolean updating = false;
	private boolean fastPaint = false;
	
	private BufferedImage background = null;
	
	public void clearPaths() {
		for (IMapLayer layer : layers.keySet()) {			
			if (layer instanceof MousePaths) {
				((MousePaths)layer).clearPaths();				
			}
		}
		background = null;
		updateLayers();		
		repaint();
	}
	
	public void addLayer(IMapLayer mapLayer) {		
		layers.put(mapLayer, null);
		updateLayers();
	}
	
	public void removeLayer(IMapLayer mapLayer) {
		layers.remove(mapLayer);		
	}
	
	public void updateLayers() {
		updating = true;
		
		if (getWidth() == 0)
			return;
		background = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
		tmp = new BufferedImage(2*(int)zoom, 2*(int)zoom, BufferedImage.TYPE_INT_ARGB);
		
		for (IMapLayer layer : layers.keySet()) {
			layers.put(layer, layer.getLayerImage(zoom));			
		}
		Graphics2D g = (Graphics2D) background.getGraphics();
		
		g.translate((int)((getWidth()-maze.getWidth()*zoom)/2),
				(int)((getHeight()-maze.getHeight()*zoom)/2));
		
		for (BufferedImage bi : layers.values()) {
			g.drawImage(bi, 0, 0, null);
		}
		getGraphics().drawImage(background, 0, 0, this);
		updating = false;				
	}
	
	public void setMouse(MousePlayer mouse) {
		if (updating)
			return;
		
		if (pathLayer != null) {
			Point2D lastPt = pathLayer.getPreviousPoint(mouse);
			pathLayer.addPoint(mouse, mouse.getPosition());			
					
			if (isPathsVisible()) {
				
				if (lastPt != null && background != null) {
					Graphics2D g = (Graphics2D) background.getGraphics();
					
					g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
					g.setColor(pathLayer.getPathColor(mouse));
					g.setStroke(new BasicStroke(2.0f));
					g.draw(new Line2D.Double(getScreenCoordinates(lastPt), getScreenCoordinates(mouse.getPosition())));
										
				}
			}
		}		
		if (!mice.containsKey(mouse.getUID())) {
			mice.put(mouse.getUID(), mouse);
			repaint();
		}
		else {
			mice.put(mouse.getUID(), mouse);
			if (getGraphics() == null || tmp == null)
				return;
			
			Point2D screen = getScreenCoordinates(mouse.getPosition());
			
			int x1 = (int) (screen.getX() - zoom);
			int y1 = (int) (screen.getY() - zoom);
			int w = 2 * (int)zoom;
			
			Graphics tmpGraphics = tmp.getGraphics();
			tmpGraphics.setColor(Color.black);
			tmpGraphics.fillRect(0, 0, tmp.getWidth(), tmp.getHeight());
			tmpGraphics.drawImage(background, 0, 0, w, w, x1, y1, x1 + w, y1 + w, this);
			
			Graphics2D g = (Graphics2D)tmp.getGraphics();
			//g.drawImage(background, x1, y1, x2, y2, x1, y1, x2, y2, this);
			g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);			
			g.translate(zoom, zoom);			
			AffineTransform trans = g.getTransform();
			g.rotate(-Math.toRadians(mouse.getDirection()-90));
			mouse.paintMouse(g, zoom);
			
			for (MousePlayer mp : mice.values()) {
				if (mp == mouse)
					continue;
				g.setTransform(trans);
				if (mp.getPosition().distance(mouse.getPosition()) < 2) {
					g.translate(zoom*(mp.getPosition().getX() - mouse.getPosition().getX()), 
							zoom*(mouse.getPosition().getY() - mp.getPosition().getY()));
					g.rotate(-Math.toRadians(mp.getDirection()-90));					
					mp.paintMouse(g, zoom);
				}
			}
			
			getGraphics().drawImage(tmp, x1, y1, x1 + w, y1 + w, 0, 0, w , w, this); 
		}
	}
	
	public void addMouse(MousePlayer mouse) {
		mice.put(mouse.getUID(),mouse);
	}
	
	
	public void clearMice() {
		mice.clear();
		MousePlayer.num = 0;
	}
	
	
	public void paintMouse(MousePlayer m, Graphics2D g) {
		
		Point2D pt = getScreenCoordinates(m.getPosition());			
		g.translate(pt.getX(), pt.getY());
		
		g.rotate(-Math.toRadians(m.getDirection()-90));
		
		m.paintMouse(g, zoom);
		
	}
	
	public synchronized  void drawMice(Graphics2D g) {
		
		AffineTransform trans = g.getTransform();
		
		for (MousePlayer m : mice.values()) {
			g.setTransform(trans);
			paintMouse(m, g);		
		}
	}
	
	public void setMaze(Maze maze) {
		
		this.maze = maze;
		
		layers.clear();
		
		if (maze != null){
			addLayer(maze);
			if (isGridEnabled())
				addLayer(new ChessLayer(maze));
			if (isBeaconVisEnabled())				
				addLayer(new BeaconVisibility(maze, maze.getBeacon()));			
			if (isPathsVisible()) {				
				pathLayer = new MousePaths(maze);
				addLayer(pathLayer);
			}
		}

		componentResized(null);
		repaint();
	}
	
	public MazePanel() {
		setBackground(Color.black);
		setMaze(null);		
		
		addMouseWheelListener(new MouseWheelListener() {
			public void mouseWheelMoved(MouseWheelEvent e) {
				if (autoZoom)
					return;
				
				int ammount = e.getUnitsToScroll();
				double lastZoom = zoom;
				if (ammount < 0)
					zoom *= 1.3;									
				else
					zoom *= (1/1.3);
				
				zoom = Math.min(60, zoom);
				zoom = Math.max(1, zoom);
				if (lastZoom != zoom) {
					updateLayers();
				}
				setPreferredSize(new Dimension((int)(MazePanel.this.maze.getWidth() * zoom), (int)(MazePanel.this.maze.getHeight() * zoom)));
				setSize((int)(MazePanel.this.maze.getWidth() * zoom), (int)(MazePanel.this.maze.getHeight() * zoom));
				repaint();
				
			}
		});
		
		addComponentListener(this);
		
		//Timer timer = new Timer("MazePanel updater");
		//timer.schedule(updater, 0, 50);		
	}
		
		public void componentResized(ComponentEvent e) {

			if (MazePanel.this.maze == null || !autoZoom || getWidth() == 0)
				return;

			double zoomW = (double)(getWidth()-6) / MazePanel.this.maze.getWidth();
			double zoomH = (double)(getHeight()-6) / MazePanel.this.maze.getHeight();

			MazePanel.this.zoom = Math.min(zoomW, zoomH);
			updateLayers();
			repaint();					
		}

	public Point2D getScreenCoordinates(Point2D worldCoordinates) {
		
		if (maze == null)
			return new Point2D.Double();
		
		Point2D.Double ret = new Point2D.Double((getWidth()-maze.getWidth()*zoom)/2.0,getHeight()-(getHeight()-maze.getHeight()*zoom)/2.0);
		ret.x = ret.x + worldCoordinates.getX() * zoom;
		ret.y = ret.y - worldCoordinates.getY() * zoom;
		return ret;
	}
	
	public void paint(Graphics g) {
		super.paint(g);		
		
		if (maze == null)
			return;
		if (background != null)
			g.drawImage(background, 0, 0, null);
		updateMousePositions(g);
	}
	private Area highWalls, lowWalls;
	
	public void fastPaint(Graphics2D g) {
		super.paint(g);
		g.translate(0, getHeight());
		g.scale(zoom, -zoom);
		if (highWalls == null) {
			highWalls = new Area();
			lowWalls = new Area();
			
			for (Wall w : maze.getWalls()) {
				if (w.getHeight() <= maze.getBeacon().getHeight())
					lowWalls.add(new Area(w.getShape()));			
				else 
					highWalls.add(new Area(w.getShape()));				
			}
		}
		g.setColor(Color.blue.darker());
		g.fill(highWalls);
		g.setColor(Color.gray.darker());
		g.fill(lowWalls);

	}
	
	public void updateMousePositions(Graphics g) {
		if (updating)
			return;
		
		Graphics2D g2 = (Graphics2D)g;		
		g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		drawMice(g2);
	}
	
	public void componentHidden(ComponentEvent e) {
		
	}
	
	public void componentMoved(ComponentEvent e) {
		
	}
	
	public void componentShown(ComponentEvent e) {
		
	}

	public boolean isGridEnabled() {
		return gridEnabled;
	}

	public void setGridEnabled(boolean gridEnabled) {
		this.gridEnabled = gridEnabled;		
		setMaze(maze);
	}

	public boolean isBeaconVisEnabled() {
		return beaconVisEnabled;
	}

	public void setBeaconVisEnabled(boolean beaconVisEnabled) {
		this.beaconVisEnabled = beaconVisEnabled;
		setMaze(maze);
	}

	public boolean isPathsVisible() {
		return pathsVisible;
	}

	public void setPathsVisible(boolean pathsVisible) {		
		this.pathsVisible = pathsVisible;
		setMaze(maze);
	}
}
